feat(kanban): card attachments#40
Conversation
Add an Attachments section to the card peek: a list of attachment links (URLs or vault paths) with add / remove, plus a file upload where the platform provides an uploader. File-board values live in frontmatter `attachments` (so they ride document sync to desktop + web); DB-board values live in properties_extra. - shared/lib/board.ts: card.attachments + parseAttachments / serializeAttachments / attachmentName helpers. - jtype-core: scan_board_cards parses `attachments` frontmatter into BoardCardInfo (parse_attachments — comma-split, kept verbatim, no #/[] stripping). - BoardPeek: Attachments list (clickable, basename label, remove ✕) + "paste URL or path" input + an Upload button when onUploadAttachment is supplied. - BoardSurface/types: thread onUploadAttachment through to the peek. - Adapters: desktop + web file boards read/write the `attachments` frontmatter; the DB board reads/writes properties_extra. Web boards wire upload to api.uploadAsset; desktop uses URL/path entry. - tests/unit/boardAttachments.spec.ts + i18n (zh). Verified: cargo check + 34 jtype-core tests, root+web tsc, unit tests, and a throwaway harness confirmed the attachment list (correct basenames + links), add-via-input, remove, and the upload control. Follow-up: desktop file upload via the blob channel; inline image previews. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds attachment support to board cards end-to-end. Shared utilities ( ChangesCard Attachments for Board
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Attachment values are user-supplied, so a `javascript:`/`data:`/`vbscript:`/`file:` "URL" rendered into `<a href>` would execute on click. Add isSafeAttachmentUrl (allows http(s) + scheme-less relative paths, blocks everything else) and render unsafe attachments as inert text marked "(unsafe)" instead of a link. - shared/lib/board.ts: isSafeAttachmentUrl helper. - BoardPeek: conditional <a> vs inert <span> for the attachment. - tests/unit/boardAttachments.spec.ts: cover safe/dangerous schemes. - i18n: new strings + zh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ments # Conflicts: # services/jtype-core/src/lib.rs # services/jtype-web/frontend/src/pages/Kanban.tsx # services/jtype-web/frontend/src/pages/WebBoardView.tsx # shared/components/board/BoardPeek.tsx # shared/components/board/BoardSurface.tsx # shared/components/board/types.ts # shared/i18n/locales/en/messages.mjs # shared/i18n/locales/en/messages.po # shared/i18n/locales/ja/messages.mjs # shared/i18n/locales/ja/messages.po # shared/i18n/locales/ko/messages.mjs # shared/i18n/locales/ko/messages.po # shared/i18n/locales/zh/messages.mjs # shared/i18n/locales/zh/messages.po # shared/lib/board.ts # src/components/BoardView.tsx # src/lib/types.ts
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@services/jtype-web/frontend/src/pages/WebBoardView.tsx`:
- Line 232: In the WebBoardView.tsx file where attachments are serialized, the
current code assigns serialized attachments to next.attachments whenever
patch.attachments is defined, but this creates an empty string when the
attachments list is cleared. Instead of always serializing when defined, add an
additional check to verify the attachments array has items (length > 0) before
calling serializeAttachments and assigning to next.attachments. If the
attachments list is empty, either omit the assignment entirely or explicitly
delete the next.attachments key to ensure consistent handling of cleared
attachments across consumers.
In `@shared/components/board/BoardPeek.tsx`:
- Around line 386-394: The isSafeAttachmentUrl() function currently allows
scheme-less URLs (starting with //) to be treated as safe, but browsers
interpret these as protocol-relative URLs that resolve to another origin,
creating a security bypass. Update the isSafeAttachmentUrl() function to
explicitly reject URLs that start with // before checking other validation
logic, ensuring these scheme-less attachment URLs are properly blocked and
displayed as unsafe.
- Around line 418-429: The onChange handler in the file input element is calling
handleUpload() with a void operator, which suppresses the returned promise and
causes unhandled rejections if the upload fails. Remove the void operator and
add proper error handling to catch any failures from handleUpload(). This can be
done by either chaining a .catch() method to handle errors, or wrapping the call
in proper async/await with try/catch. Ensure that upload failures are properly
communicated to the user instead of silently failing.
In `@shared/i18n/locales/ja/messages.po`:
- Around line 73-76: Several attachment-related strings have empty Japanese
translations (msgstr) in the ja/messages.po file at the specified line ranges.
Fill in the Japanese translations for all seven msgid entries: "Attachments",
"Paste a URL or path", "Remove", "unsafe", "Unsafe link blocked: {url}",
"Upload", and "Uploading…". Each empty msgstr needs to be populated with the
appropriate Japanese translation to ensure the UI displays correctly in Japanese
instead of falling back to English.
In `@shared/i18n/locales/ko/messages.mjs`:
- Line 1: Translate the new attachment-related message strings from English to
Korean in the messages.mjs file. Locate the following message keys in the JSON:
ONWvwQ (Upload), Pvpx7b (Paste a URL or path), t_YqKh (Remove), w7E-FA (Unsafe
link blocked with url parameter), and w_Sphq (Attachments), and replace their
English values with appropriate Korean translations to ensure the Korean locale
is fully localized.
In `@shared/lib/board.ts`:
- Around line 72-83: The parseAttachments and serializeAttachments functions use
commas as delimiters, but valid vault paths and URLs can contain commas, causing
data corruption when attachments with commas are persisted and parsed back.
Replace the comma delimiter with a safer alternative (such as newline or pipe
character) in both functions: update the split(",") call in parseAttachments and
the join(", ") call in serializeAttachments to use the new delimiter
consistently. Ensure the same delimiter change is applied to the corresponding
code in services/jtype-core/src/lib.rs at Line 1367 to maintain consistency
across both platforms.
In `@src/components/BoardView.tsx`:
- Line 181: The line where patch.attachments is processed needs to handle empty
arrays specially. When patch.attachments is an empty array, instead of calling
serializeAttachments which converts it to an empty string (causing round-trip
issues), delete the attachments key from the next object. Only call
serializeAttachments and assign to next.attachments when the attachments array
has actual content. Modify the assignment at line 181 to check if
patch.attachments is non-empty before serializing, and delete next.attachments
when the array is empty.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 48f5ed05-56bd-49e8-be00-3b0182f4debc
📒 Files selected for processing (18)
services/jtype-core/src/lib.rsservices/jtype-web/frontend/src/pages/Kanban.tsxservices/jtype-web/frontend/src/pages/WebBoardView.tsxshared/components/board/BoardPeek.tsxshared/components/board/BoardSurface.tsxshared/components/board/types.tsshared/i18n/locales/en/messages.mjsshared/i18n/locales/en/messages.poshared/i18n/locales/ja/messages.mjsshared/i18n/locales/ja/messages.poshared/i18n/locales/ko/messages.mjsshared/i18n/locales/ko/messages.poshared/i18n/locales/zh/messages.mjsshared/i18n/locales/zh/messages.poshared/lib/board.tssrc/components/BoardView.tsxsrc/lib/types.tstests/unit/boardAttachments.spec.ts
| if (patch.due !== undefined) next.due = patch.due ?? '' | ||
| if (patch.icon !== undefined) next.icon = patch.icon ?? '' | ||
| if (patch.tags !== undefined) next.tags = patch.tags.map((t) => t.label).join(', ') | ||
| if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Delete attachments when empty instead of serializing to "" (Line 232).
Writing an empty string for cleared attachments can produce inconsistent round-trips across consumers. Remove the key when list length is zero.
Suggested fix
- if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments)
+ if (patch.attachments !== undefined) {
+ if (patch.attachments.length > 0) next.attachments = serializeAttachments(patch.attachments)
+ else delete next.attachments
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments) | |
| if (patch.attachments !== undefined) { | |
| if (patch.attachments.length > 0) next.attachments = serializeAttachments(patch.attachments) | |
| else delete next.attachments | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/jtype-web/frontend/src/pages/WebBoardView.tsx` at line 232, In the
WebBoardView.tsx file where attachments are serialized, the current code assigns
serialized attachments to next.attachments whenever patch.attachments is
defined, but this creates an empty string when the attachments list is cleared.
Instead of always serializing when defined, add an additional check to verify
the attachments array has items (length > 0) before calling serializeAttachments
and assigning to next.attachments. If the attachments list is empty, either omit
the assignment entirely or explicitly delete the next.attachments key to ensure
consistent handling of cleared attachments across consumers.
| {isSafeAttachmentUrl(url) ? ( | ||
| <a href={url} target="_blank" rel="noreferrer" className="flex-1 truncate text-brand-dark hover:underline" title={url}> | ||
| {attachmentName(url)} | ||
| </a> | ||
| ) : ( | ||
| <span className="flex-1 truncate text-stone-500" title={t`Unsafe link blocked: ${url}`}> | ||
| {attachmentName(url)} <span className="text-red-500">({t`unsafe`})</span> | ||
| </span> | ||
| )} |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Reject //host attachment URLs.
isSafeAttachmentUrl() still treats scheme-less //... values as safe, but browsers resolve them to another origin. That lets a pasted attachment bypass the unsafe-link block and stay clickable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/components/board/BoardPeek.tsx` around lines 386 - 394, The
isSafeAttachmentUrl() function currently allows scheme-less URLs (starting with
//) to be treated as safe, but browsers interpret these as protocol-relative
URLs that resolve to another origin, creating a security bypass. Update the
isSafeAttachmentUrl() function to explicitly reject URLs that start with //
before checking other validation logic, ensuring these scheme-less attachment
URLs are properly blocked and displayed as unsafe.
| {onUploadAttachment && ( | ||
| <label className="inline-flex shrink-0 cursor-pointer items-center gap-1 rounded-md border border-stone-200 px-2 py-1 text-xs text-stone-600 hover:border-brand/40 hover:text-brand-dark"> | ||
| <ArrowUpTrayIcon className="h-3.5 w-3.5" /> | ||
| {uploading ? <Trans>Uploading…</Trans> : <Trans>Upload</Trans>} | ||
| <input | ||
| type="file" | ||
| className="hidden" | ||
| disabled={uploading} | ||
| onChange={(e) => { | ||
| void handleUpload(e.target.files?.[0]); | ||
| e.target.value = ""; | ||
| }} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Handle upload failures before discarding the promise.
handleUpload() can reject here, but the void call drops the promise. That turns an upload failure into an unhandled rejection and gives the user no feedback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/components/board/BoardPeek.tsx` around lines 418 - 429, The onChange
handler in the file input element is calling handleUpload() with a void
operator, which suppresses the returned promise and causes unhandled rejections
if the upload fails. Remove the void operator and add proper error handling to
catch any failures from handleUpload(). This can be done by either chaining a
.catch() method to handle errors, or wrapping the call in proper async/await
with try/catch. Ensure that upload failures are properly communicated to the
user instead of silently failing.
| #: shared/components/board/BoardPeek.tsx | ||
| msgid "Attachments" | ||
| msgstr "" | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fill Japanese translations for new attachment strings.
These newly added entries are shipped with empty msgstr, so the Japanese UI will show English fallback for the attachment flow (Attachments, Paste a URL or path, Remove, unsafe, Unsafe link blocked: {url}, Upload, Uploading…).
Also applies to: 339-342, 370-373, 482-489, 502-509
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/i18n/locales/ja/messages.po` around lines 73 - 76, Several
attachment-related strings have empty Japanese translations (msgstr) in the
ja/messages.po file at the specified line ranges. Fill in the Japanese
translations for all seven msgid entries: "Attachments", "Paste a URL or path",
"Remove", "unsafe", "Unsafe link blocked: {url}", "Upload", and "Uploading…".
Each empty msgstr needs to be populated with the appropriate Japanese
translation to ensure the UI displays correctly in Japanese instead of falling
back to English.
| @@ -1 +1 @@ | |||
| /*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"1DBGsz\":[\"노트\"],\"1YABGm\":[\"링크 (Ctrl+K)\"],\"1hKEom\":[\"우선순위\"],\"2wxgft\":[\"이름 변경\"],\"3qkggm\":[\"전체 화면\"],\"4gdyen\":[\"로컈 (내 것)\"],\"4hJhzz\":[\"테이블\"],\"54sFiP\":[\"flowchart TD\\n A[시작] --> B[끝]\"],\"5Q_DQ6\":[\"인라인 코드\"],\"7VpPHA\":[\"확인\"],\"7s3WlU\":[\"Blocks\"],\"8PifYj\":[\"Mermaid 다이어그램\"],\"8hSn0h\":[\"결과 (편집 가능)\"],\"8lE269\":[\"정렬: 수동\"],\"9gxam6\":[\"이 Draw.io 다이어그램을 렌더링할 수 없습니다.\"],\"AC9Gkf\":[\"열 펼치기\"],\"AS5WO9\":[\"이 PDF를 렌더링할 수 없습니다.\"],\"AVreQ5\":[\"드래그하여 크기 조정\"],\"AgvHni\":[\"열 추가\"],\"AxAubu\":[\"그룹: 담당자\"],\"BfMZ7w\":[\"클라우드 수낙\"],\"BnmEvM\":[\"템플릿으로 저장\"],\"C6-ZRl\":[\"Someone\"],\"EWPtMO\":[\"코드\"],\"EbMPZJ\":[\"미할당\"],\"G4qrLy\":[\"완료 열 해제\"],\"GKu3m4\":[\"라벨 없음\"],\"Gpfctt\":[\"마감\"],\"H_SQFv\":[\"색상 없음\"],\"I6SWEy\":[\"스플릿\"],\"ICip_B\":[\"클라우드 (원격)\"],\"Ik60OC\":[\"에디터에서 열기\"],\"Iw6WJa\":[\"WIP 한도 설정\"],\"JTYvAw\":[\"카드 검색\"],\"K_F6pa\":[\"저장 중…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"굵게\"],\"KvW1VO\":[\"Draw.io 다이어그램\"],\"LQn6-8\":[\"로컈 수낙\"],\"MHrjPM\":[\"제목\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"OYHzN1\":[\"태그\"],\"OepdfE\":[\"그룹: 상태\"],\"Q2mGA7\":[\"필터 지우기\"],\"QD8opX\":[\"보드\"],\"QlsPZy\":[\"Mermaid 구문을 작성하면 다이어그램이 표시됩니다.\"],\"S5Qbb1\":[\"쉼표로 구분\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"빈 카드\"],\"VNa_N2\":[\"이 파일 형식은 아직 미리볼 수 없습니다.\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"정렬: 우선순위\"],\"X03-eC\":[\"값을 입력해 주세요.\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"다이어그램 오류\"],\"Zot9XS\":[\"카드 없음\"],\"_5CsXX\":[\"완료 열\"],\"_EsjyQ\":[\"이것 사용\"],\"a6uhHr\":[\"굵게 (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"세부정보 추가...\"],\"agOeRN\":[\"이 API 명세를 렌더링할 수 없습니다.\"],\"b4hVKD\":[\"색상 열\"],\"cfaWH-\":[\"라벨 추가\"],\"cnGeoo\":[\"삭제\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP 한도 \",[\"0\"]],\"dEgA5A\":[\"취소\"],\"euc6Ns\":[\"복제\"],\"fYcKtB\":[\"정렬: 마감\"],\"gLDJuJ\":[\"제목 없는 카드\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF 문서\"],\"i4_LY_\":[\"작성\"],\"iTylMl\":[\"템플릿\"],\"iYVqZq\":[\"열 이름\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"색상\"],\"kZlRKE\":[\"Mermaid 소스\"],\"kryGs-\":[\"카드\"],\"lCF0wC\":[\"새로고침\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"병합 결과 저장\"],\"nabda1\":[\"카드 삭제\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"필터\"],\"o8va6N\":[\"Restored\"],\"ojKCLU\":[\"담당자\"],\"p9yTeb\":[\"정렬: 제목\"],\"pKztsX\":[\"전체 에디터에서 열기\"],\"pnrmSP\":[\"새 카드\"],\"pwN6Ae\":[\"열 접기\"],\"pzutoc\":[\"기울임꼴\"],\"rdUucN\":[\"미리보기\"],\"sCzmvQ\":[\"개 카드\"],\"sQpDn6\":[\"전체 화면 종료\"],\"tK2x9T\":[\"⚠ 해결할 충돌 \",[\"0\"],\"건\",[\"1\"]],\"u2IprG\":[\"카드 제목 (Enter로 추가, Esc로 취소)\"],\"uAQUqI\":[\"상태\"],\"ucJg3u\":[\"Swimlane: Status\"],\"wf6Djn\":[\"기울임꼴 (Ctrl+I)\"],\"wtw-au\":[\"완료 열로 설정\"],\"wwu18a\":[\"아이콘\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"y1eoq1\":[\"링크 복사\"],\"y9cj46\":[\"그룹: 우선순위\"],\"yEbJGs\":[\"+ Add field\"],\"ybGQtY\":[\"← 목록으로\"],\"yz7wBu\":[\"닫기\"],\"yzF66j\":[\"링크\"],\"zOc0vf\":[\"아이콘 없음\"],\"zga9sT\":[\"확인\"]}"); No newline at end of file | |||
| /*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"1DBGsz\":[\"노트\"],\"1YABGm\":[\"링크 (Ctrl+K)\"],\"1hKEom\":[\"우선순위\"],\"1lWHP7\":[\"unsafe\"],\"2wxgft\":[\"이름 변경\"],\"3qkggm\":[\"전체 화면\"],\"4gdyen\":[\"로컈 (내 것)\"],\"4hJhzz\":[\"테이블\"],\"54sFiP\":[\"flowchart TD\\n A[시작] --> B[끝]\"],\"5Q_DQ6\":[\"인라인 코드\"],\"7VpPHA\":[\"확인\"],\"7s3WlU\":[\"Blocks\"],\"8PifYj\":[\"Mermaid 다이어그램\"],\"8hSn0h\":[\"결과 (편집 가능)\"],\"8lE269\":[\"정렬: 수동\"],\"9gxam6\":[\"이 Draw.io 다이어그램을 렌더링할 수 없습니다.\"],\"AC9Gkf\":[\"열 펼치기\"],\"AS5WO9\":[\"이 PDF를 렌더링할 수 없습니다.\"],\"AVreQ5\":[\"드래그하여 크기 조정\"],\"AgvHni\":[\"열 추가\"],\"AxAubu\":[\"그룹: 담당자\"],\"BfMZ7w\":[\"클라우드 수낙\"],\"BnmEvM\":[\"템플릿으로 저장\"],\"C6-ZRl\":[\"Someone\"],\"EWPtMO\":[\"코드\"],\"EbMPZJ\":[\"미할당\"],\"G4qrLy\":[\"완료 열 해제\"],\"GKu3m4\":[\"라벨 없음\"],\"Gpfctt\":[\"마감\"],\"H_SQFv\":[\"색상 없음\"],\"I6SWEy\":[\"스플릿\"],\"ICip_B\":[\"클라우드 (원격)\"],\"Ik60OC\":[\"에디터에서 열기\"],\"Iw6WJa\":[\"WIP 한도 설정\"],\"JTYvAw\":[\"카드 검색\"],\"K_F6pa\":[\"저장 중…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"굵게\"],\"KvW1VO\":[\"Draw.io 다이어그램\"],\"LQn6-8\":[\"로컈 수낙\"],\"MHrjPM\":[\"제목\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"ONWvwQ\":[\"Upload\"],\"OYHzN1\":[\"태그\"],\"OepdfE\":[\"그룹: 상태\"],\"Pvpx7b\":[\"Paste a URL or path\"],\"Q2mGA7\":[\"필터 지우기\"],\"QD8opX\":[\"보드\"],\"QlsPZy\":[\"Mermaid 구문을 작성하면 다이어그램이 표시됩니다.\"],\"S5Qbb1\":[\"쉼표로 구분\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"빈 카드\"],\"VNa_N2\":[\"이 파일 형식은 아직 미리볼 수 없습니다.\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"정렬: 우선순위\"],\"X03-eC\":[\"값을 입력해 주세요.\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"다이어그램 오류\"],\"Zot9XS\":[\"카드 없음\"],\"_5CsXX\":[\"완료 열\"],\"_EsjyQ\":[\"이것 사용\"],\"a6uhHr\":[\"굵게 (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"세부정보 추가...\"],\"agOeRN\":[\"이 API 명세를 렌더링할 수 없습니다.\"],\"b4hVKD\":[\"색상 열\"],\"cfaWH-\":[\"라벨 추가\"],\"cnGeoo\":[\"삭제\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP 한도 \",[\"0\"]],\"dEgA5A\":[\"취소\"],\"euc6Ns\":[\"복제\"],\"fYcKtB\":[\"정렬: 마감\"],\"gANddk\":[\"Uploading…\"],\"gLDJuJ\":[\"제목 없는 카드\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF 문서\"],\"i4_LY_\":[\"작성\"],\"iTylMl\":[\"템플릿\"],\"iYVqZq\":[\"열 이름\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"색상\"],\"kZlRKE\":[\"Mermaid 소스\"],\"kryGs-\":[\"카드\"],\"lCF0wC\":[\"새로고침\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"병합 결과 저장\"],\"nabda1\":[\"카드 삭제\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"필터\"],\"o8va6N\":[\"Restored\"],\"ojKCLU\":[\"담당자\"],\"p9yTeb\":[\"정렬: 제목\"],\"pKztsX\":[\"전체 에디터에서 열기\"],\"pnrmSP\":[\"새 카드\"],\"pwN6Ae\":[\"열 접기\"],\"pzutoc\":[\"기울임꼴\"],\"rdUucN\":[\"미리보기\"],\"sCzmvQ\":[\"개 카드\"],\"sQpDn6\":[\"전체 화면 종료\"],\"tK2x9T\":[\"⚠ 해결할 충돌 \",[\"0\"],\"건\",[\"1\"]],\"t_YqKh\":[\"Remove\"],\"u2IprG\":[\"카드 제목 (Enter로 추가, Esc로 취소)\"],\"uAQUqI\":[\"상태\"],\"ucJg3u\":[\"Swimlane: Status\"],\"w7E-FA\":[\"Unsafe link blocked: \",[\"url\"]],\"w_Sphq\":[\"Attachments\"],\"wf6Djn\":[\"기울임꼴 (Ctrl+I)\"],\"wtw-au\":[\"완료 열로 설정\"],\"wwu18a\":[\"아이콘\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"y1eoq1\":[\"링크 복사\"],\"y9cj46\":[\"그룹: 우선순위\"],\"yEbJGs\":[\"+ Add field\"],\"ybGQtY\":[\"← 목록으로\"],\"yz7wBu\":[\"닫기\"],\"yzF66j\":[\"링크\"],\"zOc0vf\":[\"아이콘 없음\"],\"zga9sT\":[\"확인\"]}"); No newline at end of file | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Localize new attachment strings in the Korean catalog.
The new attachment-related entries are still English in this Korean message bundle (for example Upload, Paste a URL or path, Remove, Unsafe link blocked: {url}, Attachments), so this path won’t be fully localized.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/i18n/locales/ko/messages.mjs` at line 1, Translate the new
attachment-related message strings from English to Korean in the messages.mjs
file. Locate the following message keys in the JSON: ONWvwQ (Upload), Pvpx7b
(Paste a URL or path), t_YqKh (Remove), w7E-FA (Unsafe link blocked with url
parameter), and w_Sphq (Attachments), and replace their English values with
appropriate Korean translations to ensure the Korean locale is fully localized.
| /** Parse a frontmatter `attachments` value (comma-separated URLs/paths) into a list. */ | ||
| export function parseAttachments(raw: string): string[] { | ||
| return raw | ||
| .split(",") | ||
| .map((s) => s.trim()) | ||
| .filter(Boolean); | ||
| } | ||
|
|
||
| /** Serialize attachment URLs/paths back to a frontmatter value. */ | ||
| export function serializeAttachments(list: string[]): string { | ||
| return list.join(", "); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Use a delimiter-safe format for attachment persistence.
At Line 73 and Line 82, comma is a structural delimiter. Valid vault paths/URLs can contain commas, so one attachment can be split into multiple entries on read/write, corrupting persisted card data. The same rule is mirrored in services/jtype-core/src/lib.rs (Line 1367), so this affects both platforms.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/lib/board.ts` around lines 72 - 83, The parseAttachments and
serializeAttachments functions use commas as delimiters, but valid vault paths
and URLs can contain commas, causing data corruption when attachments with
commas are persisted and parsed back. Replace the comma delimiter with a safer
alternative (such as newline or pipe character) in both functions: update the
split(",") call in parseAttachments and the join(", ") call in
serializeAttachments to use the new delimiter consistently. Ensure the same
delimiter change is applied to the corresponding code in
services/jtype-core/src/lib.rs at Line 1367 to maintain consistency across both
platforms.
| if (patch.due !== undefined) next.due = patch.due ?? ""; | ||
| if (patch.icon !== undefined) next.icon = patch.icon ?? ""; | ||
| if (patch.tags !== undefined) next.tags = patch.tags.map((tg) => tg.label).join(", "); | ||
| if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Delete attachments when clearing the list (Line 181).
Serializing [] to "" can round-trip as a bogus empty attachment with the comma-split verbatim scanner path. This should remove the frontmatter key instead of writing an empty string.
Suggested fix
- if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments);
+ if (patch.attachments !== undefined) {
+ if (patch.attachments.length > 0) next.attachments = serializeAttachments(patch.attachments);
+ else delete next.attachments;
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/BoardView.tsx` at line 181, The line where patch.attachments
is processed needs to handle empty arrays specially. When patch.attachments is
an empty array, instead of calling serializeAttachments which converts it to an
empty string (causing round-trip issues), delete the attachments key from the
next object. Only call serializeAttachments and assign to next.attachments when
the attachments array has actual content. Modify the assignment at line 181 to
check if patch.attachments is non-empty before serializing, and delete
next.attachments when the array is empty.
What
Implements B3 — an Attachments section on the card peek: a list of attachment links (URLs or vault paths) with add / remove, plus file upload where the platform provides an uploader.
Following the data-location rule: file-board values live in frontmatter
attachments(so they ride document sync to desktop + web); DB-board values live inproperties_extra.Changes
shared/lib/board.ts—card.attachments+parseAttachments/serializeAttachments/attachmentName.jtype-core—scan_board_cardsparsesattachmentsfrontmatter intoBoardCardInfo(parse_attachments— comma-split, kept verbatim, no#/[]stripping).BoardPeek— Attachments list (clickable, basename label, remove ✕) + a "paste a URL or path" input + an Upload button whenonUploadAttachmentis supplied.BoardSurface/ types — threadonUploadAttachmentthrough to the peek.attachmentsfrontmatter; the DB board reads/writesproperties_extra. Web boards wire upload toapi.uploadAsset; desktop uses URL/path entry.tests/unit/boardAttachments.spec.ts+ i18n (zh).Verification
cargo check+ 34jtype-coretests pass; root + webtscclean; unit tests pass.spec.pdf/home.png, clickable links), add-via-input (addednotes.md), remove (✕ removedspec.pdf), and the upload control.Follow-up
Desktop file upload via the blob channel; inline image previews.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests